package buildinjava.java;

import buildinjava.BuildException;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.sun.source.tree.AnnotatedTypeTree;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.ArrayAccessTree;
import com.sun.source.tree.ArrayTypeTree;
import com.sun.source.tree.AssertTree;
import com.sun.source.tree.AssignmentTree;
import com.sun.source.tree.BinaryTree;
import com.sun.source.tree.BlockTree;
import com.sun.source.tree.BreakTree;
import com.sun.source.tree.CaseTree;
import com.sun.source.tree.CatchTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.CompoundAssignmentTree;
import com.sun.source.tree.ConditionalExpressionTree;
import com.sun.source.tree.ContinueTree;
import com.sun.source.tree.DoWhileLoopTree;
import com.sun.source.tree.EmptyStatementTree;
import com.sun.source.tree.EnhancedForLoopTree;
import com.sun.source.tree.ErroneousTree;
import com.sun.source.tree.ExpressionStatementTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.ForLoopTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.IfTree;
import com.sun.source.tree.ImportTree;
import com.sun.source.tree.InstanceOfTree;
import com.sun.source.tree.LabeledStatementTree;
import com.sun.source.tree.LineMap;
import com.sun.source.tree.LiteralTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.ModifiersTree;
import com.sun.source.tree.NewArrayTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.ParameterizedTypeTree;
import com.sun.source.tree.ParenthesizedTree;
import com.sun.source.tree.PrimitiveTypeTree;
import com.sun.source.tree.ReturnTree;
import com.sun.source.tree.StatementTree;
import com.sun.source.tree.SwitchTree;
import com.sun.source.tree.SynchronizedTree;
import com.sun.source.tree.ThrowTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.TreeVisitor;
import com.sun.source.tree.TryTree;
import com.sun.source.tree.TypeCastTree;
import com.sun.source.tree.TypeParameterTree;
import com.sun.source.tree.UnaryTree;
import com.sun.source.tree.VariableTree;
import com.sun.source.tree.WhileLoopTree;
import com.sun.source.tree.WildcardTree;
import com.sun.source.util.JavacTask;
import com.sun.source.util.SimpleTreeVisitor;
import com.sun.source.util.SourcePositions;
import com.sun.source.util.TreeScanner;
import com.sun.source.util.Trees;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Writer;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler.CompilationTask;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import org.fest.reflect.core.Reflection;
import org.fest.reflect.exception.ReflectionError;
import org.fest.reflect.type.Type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Methods for processing Java source-code.
 *
 * @author Gili Tzabari
 */
public class JavaSource extends Date
{
//	/**
//	 * A package variable scope.
//	 *
//	 * @author Gili Tzabari
//	 */
//	private static class PackageScope implements VariableScope
//	{
//		private final String name;
//
//		/**
//		 * Creates a new package.
//		 *
//		 * @param name the package name
//		 */
//		public PackageScope(String name)
//		{
//			this.name = name;
//		}
//
//		@Override
//		public String getName()
//		{
//			return name;
//		}
//
////		@Override
////		public boolean isVariable(Name identifier)
////		{
////			return false;
////		}
//		@Override
//		public boolean isTopLevel()
//		{
//			return true;
//		}
//	}
//	/**
//	 * A class variable scope.
//	 *
//	 * @author Gili Tzabari
//	 */
//	private static class ClassScope implements VariableScope
//	{
//		private final ClassVisitor visitor;
//
//		/**
//		 * Creates a new class scope.
//		 *
//		 * @param visitor the class visitor
//		 */
//		public ClassScope(ClassVisitor visitor)
//		{
//			this.visitor = visitor;
//		}
//
//		@Override
//		public String getName()
//		{
//			return visitor.getQualifiedName().getName();
//		}
//
////		@Override
////		public boolean isVariable(Name identifier)
////		{
////			return visitor.isVariable(identifier);
////		}
//		@Override
//		public boolean isTopLevel()
//		{
//			return false;
//		}
//	}
//
	/**
	 * Visits a source-code file.
	 *
	 * @author Gili Tzabari
	 */
	private static class FileVisitor extends SimpleTreeVisitor<Set<Identifier>, Parser>
	{
		private final ClassLoader loader;
		private final Map<String, String> simpleToQualifiedName = Maps.newHashMap();

		/**
		 * Creates a new FileVisitor.
		 *
		 * @param sourcePath the source code search path
		 * @param loader the class-loader
		 */
		public FileVisitor(Set<File> sourcePath, ClassLoader loader)
		{
			this.loader = loader;
			// TODO: use sourcePath
		}

		@Override
		public Set<Identifier> visitCompilationUnit(CompilationUnitTree current, Parser parser)
		{
			for (ImportTree importTree: current.getImports())
				importTree.accept(this, parser);
			for (AnnotationTree annotation: current.getPackageAnnotations())
				annotation.accept(this, parser);

			String classPrefix;
			if (current.getPackageName() == null)
				classPrefix = "";
			else
				classPrefix = current.getPackageName().toString() + ".";
			Set<Identifier> result = Sets.newHashSet();
			Set<Identifier> definedClasses = Sets.newHashSet();
			for (Tree type: current.getTypeDecls())
			{
				ClassVisitor visitor = new ClassVisitor(classPrefix, Lists.<Set<String>>newLinkedList(),
					simpleToQualifiedName, loader);
				result.addAll(type.accept(visitor, parser));
				definedClasses.addAll(getDefinedClasses(visitor));
			}

			// Remove dependencies on classes residing in the same file
			result.removeAll(definedClasses);
			return result;
		}

		@Override
		public Set<Identifier> visitImport(ImportTree current, Parser parser)
		{
			String identifier = current.getQualifiedIdentifier().toString();
			int lastDot = identifier.lastIndexOf(".");
			simpleToQualifiedName.put(identifier.substring(lastDot + ".".length()), identifier);
			return ImmutableSet.of();
		}

		@Override
		public Set<Identifier> visitAnnotation(AnnotationTree current, Parser parser)
		{
			current.getAnnotationType().accept(new ExpressionVisitor(), parser);
			for (ExpressionTree argument: current.getArguments())
				argument.accept(new ExpressionVisitor(), parser);
			return ImmutableSet.of();
		}

		/**
		 * Returns the class names defined by the class or nested classes.
		 *
		 * @param visitor the class
		 * @return the class names defined by the class or nested classes
		 */
		private Set<Identifier> getDefinedClasses(ClassVisitor visitor)
		{
			Set<Identifier> result = Sets.newHashSet();
			result.add(visitor.getQualifiedName());
			for (ClassVisitor nested: visitor.getNestedClasses())
				result.addAll(getDefinedClasses(nested));
			return result;
		}
	}

	/**
	 * Generates a list of classes referenced by an expression.
	 *
	 * @author Gili Tzabari
	 */
	private static class ExpressionVisitor extends TreeScanner<Void, Parser>
	{
		@Override
		public Void scan(Tree tree, Parser parser)
		{
			// TODO: investigate using testcase
			throw new AssertionError(tree);
		}
	}

	/**
	 * Used by getDependencies(File, ClassLoader).
	 *
	 * @author Gili Tzabari
	 */
	private static class ClassVisitor implements TreeVisitor<Set<Identifier>, Parser>
	{
		private final Map<String, String> simpleToQualifiedName;
		private final Logger log = LoggerFactory.getLogger(JavaSource.class);
		private final ClassLoader loader;
		private final Set<ClassVisitor> nestedClasses = Sets.newHashSet();
		/**
		 * Variables names indexed from the inner-most to the outer-most scope.
		 */
		private final List<Set<String>> variableNames;
		private String classPrefix;//VariableScope enclosingScope;
		private Identifier className;
		/**
		 * The recursive union of all supertypes extended by a class.
		 */
		private Set<Class<?>> superTypes = Sets.newHashSet();

		/**
		 * Creates a new ClassVisitor.
		 *
		 * @param enclosingScope the enclosing variable scope
		 * @param variableNames variables names indexed from the inner-most to the outer-most scope.
		 * @param classPrefix the prefix that should be applied to class names
		 * @param simpleToQualifiedName maps simple to fully-qualified class names
		 * @param loader the class-loader
		 */
		public ClassVisitor(String classPrefix, List<Set<String>> variableNames,
												Map<String, String> simpleToQualifiedName,
												ClassLoader loader) //VariableScope enclosingScope,
		{
			//this.enclosingScope = enclosingScope;
			this.classPrefix = classPrefix;
			this.variableNames = variableNames;
			this.simpleToQualifiedName = Maps.newHashMap(simpleToQualifiedName);
			this.loader = loader;
		}

		/**
		 * Returns the simple class name.
		 *
		 * @return the simple class name
		 */
		public String getSimpleName()
		{
			int normalSeparator = className.getName().lastIndexOf(".");
			int nestedSeparator = className.getName().lastIndexOf("$");
			int separator = Math.max(normalSeparator, nestedSeparator);
			if (separator == -1)
				return "";
			return className.getName().substring(separator + 1);
		}

		/**
		 * Returns the fully-qualified class name.
		 *
		 * @return the fully-qualified class name
		 */
		public Identifier getQualifiedName()
		{
			return className;
		}

		/**
		 * Returns the classes nested in this class.
		 *
		 * @return the nested classes
		 */
		private Set<ClassVisitor> getNestedClasses()
		{
			return nestedClasses;
		}

		@Override
		public Set<Identifier> visitAnnotation(AnnotationTree current, Parser parser)
		{
			Set<Identifier> result = Sets.newHashSet();
			result.addAll(current.getAnnotationType().accept(this, parser));
			for (ExpressionTree argument: current.getArguments())
				result.addAll(argument.accept(this, parser));
			return result;
		}

		@Override
		public Set<Identifier> visitAnnotatedType(AnnotatedTypeTree current, Parser parser)
		{
			Set<Identifier> result = Sets.newHashSet();
			for (AnnotationTree annotation: current.getAnnotations())
				result.addAll(annotation.accept(this, parser));
			result.addAll(current.getUnderlyingType().accept(this, parser));
			return result;
		}

		@Override
		public Set<Identifier> visitMethodInvocation(MethodInvocationTree current,
																								 Parser parser)
		{
			Set<Identifier> result = Sets.newHashSet();
			for (Tree type: current.getTypeArguments())
				result.addAll(type.accept(this, parser));

			ExpressionTree methodSelect = current.getMethodSelect();
			if (!(methodSelect instanceof IdentifierTree))
			{
				// if methodSelect is an identifier it refers to a method name and does not contain
				// dependencies.
				result.addAll(current.getMethodSelect().accept(this, parser));
			}

			for (ExpressionTree argument: current.getArguments())
				result.addAll(argument.accept(this, parser));
			return result;
		}

		@Override
		public Set<Identifier> visitAssert(AssertTree current, Parser parser)
		{
			Set<Identifier> result = Sets.newHashSet();
			result.addAll(current.getCondition().accept(this, parser));
			if (current.getDetail() != null)
				result.addAll(current.getDetail().accept(this, parser));
			return result;
		}

		@Override
		public Set<Identifier> visitAssignment(AssignmentTree current, Parser parser)
		{
			Set<Identifier> result = Sets.newHashSet();
			result.addAll(current.getVariable().accept(this, parser));
			result.addAll(current.getExpression().accept(this, parser));
			return result;
		}

		@Override
		public Set<Identifier> visitCompoundAssignment(CompoundAssignmentTree current,
																									 Parser parser)
		{
			Set<Identifier> result = Sets.newHashSet();
			result.addAll(current.getVariable().accept(this, parser));
			result.addAll(current.getExpression().accept(this, parser));
			return result;
		}

		@Override
		public Set<Identifier> visitBinary(BinaryTree current, Parser parser)
		{
			Set<Identifier> result = Sets.newHashSet();
			result.addAll(current.getLeftOperand().accept(this, parser));
			result.addAll(current.getRightOperand().accept(this, parser));
			return result;
		}

		/**
		 * Adds a new inner-most variable scope.
		 */
		private void pushScope()
		{
			variableNames.add(0, Sets.<String>newHashSet());
		}

		/**
		 * Removes any identifiers referencing the current scope, followed by removing the scope itself.
		 *
		 * @param identifiers the identifiers found in the current scope
		 */
		private void popScope(Set<Identifier> identifiers)
		{
			Set<String> localVariables = variableNames.remove(0);
			for (Iterator<Identifier> i = identifiers.iterator(); i.hasNext();)
			{
				// In case of MemberSelect identifiers, only examine the first token
				String[] tokens = i.next().name.split("\\.");
				if (localVariables.contains(tokens[0]))
					i.remove();
			}
		}

		@Override
		public Set<Identifier> visitBlock(BlockTree current, Parser parser)
		{
			pushScope();
			Set<Identifier> result = Sets.newHashSet();
			for (StatementTree statement: current.getStatements())
				result.addAll(statement.accept(this, parser));
			popScope(result);
			return result;
		}

		@Override
		public Set<Identifier> visitBreak(BreakTree current, Parser parser)
		{
			return ImmutableSet.of();
		}

		@Override
		public Set<Identifier> visitCase(CaseTree current, Parser parser)
		{
			Set<Identifier> result = Sets.newHashSet();
			result.addAll(current.getExpression().accept(this, parser));
			for (StatementTree statement: current.getStatements())
				result.addAll(statement.accept(this, parser));
			return result;
		}

		@Override
		public Set<Identifier> visitCatch(CatchTree current, Parser parser)
		{
			pushScope();
			Set<Identifier> result = Sets.newHashSet();
			result.addAll(current.getParameter().accept(this, parser));
			result.addAll(current.getBlock().accept(this, parser));
			popScope(result);
			return result;
		}

		@Override
		public Set<Identifier> visitClass(ClassTree current, Parser parser)
		{
			pushScope();
			assert (className == null): className;

			String nestedClassPrefix;
			if (current.getSimpleName().toString().isEmpty())
			{
				nestedClassPrefix = "";
				className = new Identifier(nestedClassPrefix, current, parser);
			}
			else
			{
				className = new Identifier(classPrefix + current.getSimpleName().toString(), current,
					parser);
				nestedClassPrefix = className.getName() + "$";
			}
			if (!className.getName().isEmpty())
				simpleToQualifiedName.put(current.getSimpleName().toString(), className.getName());

			Set<Identifier> dependencies = Sets.newHashSet();
			for (TypeParameterTree parameter: current.getTypeParameters())
			{
				variableNames.get(0).add(parameter.getName().toString());
				dependencies.addAll(parameter.accept(this, parser));
			}
			Set<Identifier> superTypeIdentifiers = Sets.newHashSet();
			if (current.getExtendsClause() != null)
			{
				superTypeIdentifiers.add(current.getExtendsClause().accept(new GetRawType(), parser));
				dependencies.addAll(current.getExtendsClause().accept(this, parser));
			}
			for (Tree type: current.getImplementsClause())
			{
				superTypeIdentifiers.add(type.accept(new GetRawType(), parser));
				dependencies.addAll(type.accept(this, parser));
			}
			dependencies.addAll(current.getModifiers().accept(this, parser));

			for (Tree member: current.getMembers())
			{
				if (member instanceof ClassTree)
				{
					ClassVisitor nestedClass = new ClassVisitor(nestedClassPrefix, variableNames,
						simpleToQualifiedName, loader);
					dependencies.addAll(member.accept(nestedClass, parser));
					if (!className.getName().isEmpty())
					{
						simpleToQualifiedName.put(nestedClass.getSimpleName(), nestedClass.getQualifiedName().
							getName());
					}
					nestedClasses.add(nestedClass);
				}
				else
					dependencies.addAll(member.accept(this, parser));
			}
			dependencies.remove(new Identifier("this", current, parser));

			Set<Identifier> result = Sets.newHashSet();
			for (Identifier typeIdentifier: superTypeIdentifiers)
			{
				Identifier qualifiedName = getQualifiedName(typeIdentifier);
				log.trace("Mapping \"" + typeIdentifier.getName() + "\" to \"" + qualifiedName.getName()
									+ "\" inside \"" + className.getName() + "\"");
				Class<?> type = Reflection.type(qualifiedName.getName()).withClassLoader(loader).load();
				superTypes.add(type);
				superTypes.addAll(getSuperTypes(type));
			}
			popScope(dependencies);
			if (variableNames.isEmpty())
			{
				for (Identifier dependency: dependencies)
				{
					Identifier qualifiedName = getQualifiedName(dependency);
					log.trace("Mapping \"" + dependency.getName() + "\" to \"" + qualifiedName.getName()
										+ "\" inside \"" + qualifiedName.getName() + "\"");
					result.add(qualifiedName);
				}
			}
			else
				result.addAll(dependencies);
			return result;
		}

		@Override
		public Set<Identifier> visitConditionalExpression(ConditionalExpressionTree current,
																											Parser parser)
		{
			Set<Identifier> result = Sets.newHashSet();
			result.addAll(current.getCondition().accept(this, parser));
			result.addAll(current.getTrueExpression().accept(this, parser));
			result.addAll(current.getFalseExpression().accept(this, parser));
			return result;
		}

		@Override
		public Set<Identifier> visitContinue(ContinueTree current, Parser parser)
		{
			return ImmutableSet.of();
		}

		@Override
		public Set<Identifier> visitDoWhileLoop(DoWhileLoopTree current, Parser parser)
		{
			Set<Identifier> result = Sets.newHashSet();
			result.addAll(current.getStatement().accept(this, parser));
			result.addAll(current.getCondition().accept(this, parser));
			return result;
		}

		@Override
		public Set<Identifier> visitErroneous(ErroneousTree current, Parser parser)
		{
			return ImmutableSet.of();
		}

		@Override
		public Set<Identifier> visitExpressionStatement(ExpressionStatementTree current,
																										Parser parser)
		{
			return current.getExpression().accept(this, parser);
		}

		@Override
		public Set<Identifier> visitEnhancedForLoop(EnhancedForLoopTree current, Parser parser)
		{
			Set<Identifier> result = Sets.newHashSet();
			result.addAll(current.getVariable().accept(this, parser));
			result.addAll(current.getExpression().accept(this, parser));
			result.addAll(current.getStatement().accept(this, parser));
			return result;
		}

		@Override
		public Set<Identifier> visitForLoop(ForLoopTree current, Parser parser)
		{
			Set<Identifier> result = Sets.newHashSet();
			for (StatementTree initializer: current.getInitializer())
				result.addAll(initializer.accept(this, parser));
			result.addAll(current.getCondition().accept(this, parser));
			for (ExpressionStatementTree initializer: current.getUpdate())
				result.addAll(initializer.accept(this, parser));
			result.addAll(current.getStatement().accept(this, parser));
			return result;
		}

//		/**
//		 * Indicates if an identifier refers to a variable.
//		 *
//		 * @param identifier the identifier name
//		 * @return true if the identifier refers to a variable
//		 */
//		private boolean isVariable(String identifier)
//		{
////			if (enclosingScope.isVariable(identifier))
////				return true;
//			for (Set<String> scope: variableNames)
//				if (scope.contains(identifier))
//					return true;
//			return false;
//		}
		@Override
		public Set<Identifier> visitIdentifier(IdentifierTree current, Parser parser)
		{
//			if (isVariable(current.getName()))
//				return ImmutableSet.of();
			return ImmutableSet.of(new Identifier(current.getName().toString(), current, parser));
		}

		@Override
		public Set<Identifier> visitIf(IfTree current, Parser parser)
		{
			Set<Identifier> result = Sets.newHashSet();
			result.addAll(current.getCondition().accept(this, parser));
			result.addAll(current.getThenStatement().accept(this, parser));
			StatementTree elseStatement = current.getElseStatement();
			if (elseStatement != null)
				result.addAll(elseStatement.accept(this, parser));
			return result;
		}

		@Override
		public Set<Identifier> visitImport(ImportTree current, Parser parser)
		{
			String identifier = current.getQualifiedIdentifier().toString();
			int lastDot = identifier.lastIndexOf(".");
			simpleToQualifiedName.put(identifier.substring(lastDot + ".".length()), identifier);

			// Covered by other visitor methods
			return ImmutableSet.of();
		}

		@Override
		public Set<Identifier> visitArrayAccess(ArrayAccessTree current, Parser parser)
		{
			Set<Identifier> result = Sets.newHashSet();
			result.addAll(current.getExpression().accept(this, parser));
			result.addAll(current.getIndex().accept(this, parser));
			return result;
		}

		@Override
		public Set<Identifier> visitLabeledStatement(LabeledStatementTree current,
																								 Parser parser)
		{
			return current.getStatement().accept(this, parser);
		}

		@Override
		public Set<Identifier> visitLiteral(LiteralTree current, Parser parser)
		{
			return ImmutableSet.of();
		}

		@Override
		public Set<Identifier> visitMethod(MethodTree current, Parser parser)
		{
			pushScope();
			Set<Identifier> result = Sets.newHashSet();
			result.addAll(current.getModifiers().accept(this, parser));
			for (TypeParameterTree parameter: current.getTypeParameters())
			{
				variableNames.get(0).add(parameter.getName().toString());
				result.addAll(parameter.accept(this, parser));
			}
			if (current.getReturnType() != null)
				result.addAll(current.getReturnType().accept(this, parser));
			for (VariableTree parameter: current.getParameters())
				result.addAll(parameter.accept(this, parser));
			if (current.getBody() != null)
				result.addAll(current.getBody().accept(this, parser));
			if (current.getDefaultValue() != null)
				result.addAll(current.getDefaultValue().accept(this, parser));
			for (ExpressionTree exception: current.getThrows())
				result.addAll(exception.accept(this, parser));
			popScope(result);
			return result;
		}

		@Override
		public Set<Identifier> visitModifiers(ModifiersTree current, Parser parser)
		{
			Set<Identifier> result = Sets.newHashSet();
			for (AnnotationTree annotation: current.getAnnotations())
				result.addAll(annotation.accept(this, parser));
			return result;
		}

		@Override
		public Set<Identifier> visitNewArray(NewArrayTree current, Parser parser)
		{
			Set<Identifier> result = Sets.newHashSet();
			Tree type = current.getType();
			if (type != null)
			{
				// Annotations make use of an untyped array
				result.addAll(type.accept(this, parser));
			}
			for (ExpressionTree dimension: current.getDimensions())
				result.addAll(dimension.accept(this, parser));
			if (current.getInitializers() != null)
			{
				for (ExpressionTree initializer: current.getInitializers())
					result.addAll(initializer.accept(this, parser));
			}
			return result;
		}

		@Override
		public Set<Identifier> visitNewClass(NewClassTree current, Parser parser)
		{
			Set<Identifier> result = Sets.newHashSet();
			if (current.getEnclosingExpression() != null)
				result.addAll(current.getEnclosingExpression().accept(this, parser));
			result.addAll(current.getIdentifier().accept(this, parser));
			for (ExpressionTree argument: current.getArguments())
				result.addAll(argument.accept(this, parser));
			for (Tree typeArgument: current.getTypeArguments())
				result.addAll(typeArgument.accept(this, parser));
			if (current.getClassBody() != null)
			{
				ClassVisitor visitor = new ClassVisitor(classPrefix, variableNames, simpleToQualifiedName,
					loader);
				result.addAll(current.getClassBody().accept(visitor, parser));
			}
			return result;
		}

		@Override
		public Set<Identifier> visitParenthesized(ParenthesizedTree current, Parser parser)
		{
			return current.getExpression().accept(this, parser);
		}

		@Override
		public Set<Identifier> visitReturn(ReturnTree current, Parser parser)
		{
			ExpressionTree expression = current.getExpression();
			if (expression == null)
				return ImmutableSet.of();
			return expression.accept(this, parser);
		}

		@Override
		public Set<Identifier> visitMemberSelect(MemberSelectTree current, Parser parser)
		{
			ExpressionTree expression = current.getExpression();
			Set<Identifier> result = expression.accept(this, parser);
			if (result.isEmpty())
				return ImmutableSet.of();
			if (expression instanceof IdentifierTree)// && !isVariable(current.getIdentifier()))
			{
				return Sets.newHashSet(new Identifier(result.iterator().next().getName() + "." + current.
					getIdentifier().toString(), current, parser));
			}
			return result;
		}

		@Override
		public Set<Identifier> visitEmptyStatement(EmptyStatementTree current, Parser parser)
		{
			return ImmutableSet.of();
		}

		@Override
		public Set<Identifier> visitSwitch(SwitchTree current, Parser parser)
		{
			Set<Identifier> result = Sets.newHashSet();
			result.addAll(current.getExpression().accept(this, parser));
			for (CaseTree caseTree: current.getCases())
				result.addAll(caseTree.accept(this, parser));
			return result;
		}

		@Override
		public Set<Identifier> visitSynchronized(SynchronizedTree current, Parser parser)
		{
			Set<Identifier> result = Sets.newHashSet();
			result.addAll(current.getBlock().accept(this, parser));
			result.addAll(current.getExpression().accept(this, parser));
			return result;
		}

		@Override
		public Set<Identifier> visitThrow(ThrowTree current, Parser parser)
		{
			return current.getExpression().accept(this, parser);
		}

		@Override
		public Set<Identifier> visitCompilationUnit(CompilationUnitTree current, Parser parser)
		{
			throw new AssertionError();
		}

		/**
		 * Returns the recursive union of all supertypes extended by a class.
		 *
		 * @param clazz the class
		 * @return an empty set of clazz is java.lang.Object
		 */
		private Set<Class<?>> getSuperTypes(Class<?> clazz)
		{
			Set<Class<?>> result = Sets.newHashSet();
			Class<?> superClass = clazz.getSuperclass();
			if (superClass != null)
				result.add(superClass);
			result.addAll(Arrays.asList(clazz.getInterfaces()));
			return result;
		}

		/**
		 * Converts a simple class name to a qualified class name.
		 *
		 * @param identifier a class name
		 * @return the fully-qualified class name
		 */
		private Identifier getQualifiedName(Identifier identifier)
		{
			// If import statement is missing, identifier may correspond to:
			//
			// 1. variable declared by a supertype
			// 2. class in current package
			// 3. nested class
			// 4. nested class in super class or super interfaces
			// 5. class in java.lang package
			//
			// REFERENCE: http://stackoverflow.com/questions/1979428/java-identifier-resolution-rules/1979658#1979658

			String simpleName = identifier.getName();
			if (simpleName.contains("."))
				return identifier;

			// 1. variable declared by a supertype
			for (Class<?> type: superTypes)
			{
				try
				{
					Field field = type.getDeclaredField(simpleName);
					int modifiers = field.getModifiers();
					if (Modifier.isPrivate(modifiers))
						return null;
					return identifier.setName(type.getName() + "." + simpleName);
				}
				catch (NoSuchFieldException unused)
				{
					// do nothing
				}
			}

			// 2. class in current package
			String qualifiedName = classPrefix + simpleName;
			try
			{
				Type.newType(qualifiedName).withClassLoader(loader).load();
				simpleToQualifiedName.put(simpleName, qualifiedName);
				return identifier.setName(qualifiedName);
			}
			catch (ReflectionError unused)
			{
				// class does not exist
			}

			// 3. nested class
			qualifiedName = simpleToQualifiedName.get(simpleName);
			if (qualifiedName != null)
				return identifier.setName(qualifiedName);

			// 4. nested class in super class or super interfaces
			for (Class<?> type: superTypes)
			{
				qualifiedName = type.getName() + "$" + simpleName;
				try
				{
					Type.newType(qualifiedName).withClassLoader(loader).load();
					simpleToQualifiedName.put(simpleName, qualifiedName);
					return identifier.setName(qualifiedName);
				}
				catch (ReflectionError unused)
				{
					// class does not exist
				}
			}

			// 5. class in java.lang package
			qualifiedName = "java.lang." + simpleName;
			try
			{
				Type.newType(qualifiedName).withClassLoader(loader).load();
				simpleToQualifiedName.put(simpleName, qualifiedName);
				return identifier.setName(qualifiedName);
			}
			catch (ReflectionError unused)
			{
				// class does not exist
			}
			throw new AssertionError(simpleName + " at " + identifier.getFile().getSourceFile()
															 + ":" + identifier.getY());
		}

		@Override
		public Set<Identifier> visitTry(TryTree current, Parser parser)
		{
			Set<Identifier> result = Sets.newHashSet();
			result.addAll(current.getBlock().accept(this, parser));
			for (CatchTree catchTree: current.getCatches())
				result.addAll(catchTree.accept(this, parser));
			BlockTree finallyBlock = current.getFinallyBlock();
			if (finallyBlock != null)
				result.addAll(finallyBlock.accept(this, parser));
			return result;
		}

		@Override
		public Set<Identifier> visitParameterizedType(ParameterizedTypeTree current,
																									Parser parser)
		{
			Set<Identifier> result = Sets.newHashSet();
			result.addAll(current.getType().accept(this, parser));
			for (Tree typeArgument: current.getTypeArguments())
				result.addAll(typeArgument.accept(this, parser));
			return result;
		}

		@Override
		public Set<Identifier> visitArrayType(ArrayTypeTree current, Parser parser)
		{
			return current.getType().accept(this, parser);
		}

		@Override
		public Set<Identifier> visitTypeCast(TypeCastTree current, Parser parser)
		{
			Set<Identifier> result = Sets.newHashSet();
			result.addAll(current.getType().accept(this, parser));
			result.addAll(current.getExpression().accept(this, parser));
			return result;
		}

		@Override
		public Set<Identifier> visitPrimitiveType(PrimitiveTypeTree ptt, Parser parser)
		{
			return ImmutableSet.of();
		}

		@Override
		public Set<Identifier> visitTypeParameter(TypeParameterTree current, Parser parser)
		{
			Set<Identifier> result = Sets.newHashSet();
			for (Tree bound: current.getBounds())
				result.addAll(bound.accept(this, parser));
			return result;
		}

		@Override
		public Set<Identifier> visitInstanceOf(InstanceOfTree current, Parser parser)
		{
			Set<Identifier> result = Sets.newHashSet();
			result.addAll(current.getExpression().accept(this, parser));
			result.addAll(current.getType().accept(this, parser));
			return result;
		}

		@Override
		public Set<Identifier> visitUnary(UnaryTree current, Parser parser)
		{
			return current.getExpression().accept(this, parser);
		}

		@Override
		public Set<Identifier> visitVariable(VariableTree current, Parser parser)
		{
			Set<Identifier> result = Sets.newHashSet();
			result.addAll(current.getModifiers().accept(this, parser));
			result.addAll(current.getType().accept(this, parser));
			variableNames.get(0).add(current.getName().toString());
			if (current.getInitializer() != null)
				result.addAll(current.getInitializer().accept(this, parser));
			return result;
		}

		@Override
		public Set<Identifier> visitWhileLoop(WhileLoopTree current, Parser parser)
		{
			Set<Identifier> result = Sets.newHashSet();
			result.addAll(current.getCondition().accept(this, parser));
			result.addAll(current.getStatement().accept(this, parser));
			return result;
		}

		@Override
		public Set<Identifier> visitWildcard(WildcardTree current, Parser parser)
		{
			Tree bound = current.getBound();
			if (bound == null)
				return ImmutableSet.of();
			return bound.accept(this, parser);
		}

		@Override
		public Set<Identifier> visitOther(Tree current, Parser parser)
		{
			return ImmutableSet.of();
		}
	}

	/**
	 * Returns a raw class type (strips type parameters if any are present).
	 *
	 * @author Gili Tzabari
	 */
	private static class GetRawType extends SimpleTreeVisitor<Identifier, Parser>
	{
		@Override
		protected Identifier defaultAction(Tree tree, Parser parser)
		{
			throw new AssertionError(tree);
		}

		@Override
		public Identifier visitIdentifier(IdentifierTree current, Parser parser)
		{
			return new Identifier(current.getName().toString(), current, parser);
		}

		@Override
		public Identifier visitParameterizedType(ParameterizedTypeTree current, Parser parser)
		{
			return current.getType().accept(this, parser);
		}

		@Override
		public Identifier visitMemberSelect(MemberSelectTree current, Parser parser)
		{
			return new Identifier(current.getExpression().accept(this, parser).getName() + "."
														+ current.getIdentifier().toString(), current, parser);
		}
	}

	/**
	 * Prevent construction.
	 */
	private JavaSource()
	{
	}

	/**
	 * Returns the dependencies of a source code file.
	 *
	 * @param sourceFile the source code whose dependencies to return
	 * @param visitor the visitor used to parse sourceFile
	 * @return a set of fully-qualified class names
	 * @throws FileNotFoundException if sourceFile does not exist
	 * @throws IOException if an I/O error occurs while parsing the source files
	 * @throws IllegalArgumentException if visitor returned null while parsing the compilation unit
	 */
	private static <R> R parse(File sourceFile, TreeVisitor<R, Parser> visitor)
		throws FileNotFoundException, IOException
	{
		if (!sourceFile.exists())
			throw new FileNotFoundException("sourceFile not found: " + sourceFile.getAbsolutePath());
		javax.tools.JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
		if (compiler == null)
		{
			throw new BuildException("javax.tools.JavaCompiler is not available. Are you running from the JRE "
															 + "instead of the JDK?");
		}
		DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<JavaFileObject>();
		StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null);
		final Iterable<? extends JavaFileObject> compilationUnits =
			fileManager.getJavaFileObjectsFromFiles(Lists.newArrayList(sourceFile));
		final Writer output = null;
		final Set<String> options = null;
		final CompilationTask task = compiler.getTask(output, fileManager, diagnostics, options, null,
			compilationUnits);
		JavacTask javacTask = (JavacTask) task;
		List<? extends CompilationUnitTree> parsers = Lists.newArrayList(javacTask.parse());
		assert (parsers.size() == 1): parsers;

		CompilationUnitTree compilationUnit = parsers.iterator().next();
		Parser parser = new Parser(compilationUnit, task);
		R result = compilationUnit.accept(visitor, parser);
		if (result == null)
		{
			throw new IllegalArgumentException(
				"visitor may not return null while parsing the compilation unit. Sourcefile=" + sourceFile);
		}
		return result;
	}

	/**
	 * Returns the dependencies of a source code file. Uses the source-code search path to parse
	 * source files for dependencies. Uses loader to check for the existence of classes, fields, etc.
	 *
	 * PRECONDITIONS: all files found in the sourcePath must be compiled and up-to-date in the
	 * classpath
	 *
	 * @param sourceFile the source code whose dependencies to return
	 * @param sourcePath the source code search path
	 * @param loader the class-loader
	 * @return a set of fully-qualified class names
	 * @throws FileNotFoundException if sourceFile does not exist
	 * @throws IOException if an I/O error occurs while parsing the source files
	 */
	public static Set<String> getDependencies(File sourceFile, Collection<File> sourcePath,
																						ClassLoader loader)
		throws FileNotFoundException, IOException
	{
		Set<String> result = Sets.newHashSet();
		for (Identifier identifier: parse(sourceFile, new FileVisitor(ImmutableSet.copyOf(sourcePath),
			loader)))
		{
			result.add(identifier.getName());
		}
		return result;
	}

//	/**
//	 * A variable scope.
//	 *
//	 * @author Gili Tzabari
//	 */
//	private static interface VariableScope
//	{
//		/**
//		 * Returns the name of the enclosing type.
//		 *
//		 * @return the name of the enclosing type
//		 */
//		String getName();
//
//		/**
//		 * Indicates if an identifier refers to a variable in the scope.
//		 *
//		 * @param identifier the identifier name
//		 * @return true if the identifier refers to a variable
//		 */
////		boolean isVariable(Name identifier);
//		/**
//		 * Indicates if the object refers to the outer-most scope.
//		 *
//		 * @return true if the object refers to the outer-most scope
//		 */
//		boolean isTopLevel();
//	}
	/**
	 * Returns the package being visited.
	 *
	 * @author Gili Tzabari
	 */
	private static class GetPackage extends SimpleTreeVisitor<String, Parser>
	{
		@Override
		public String visitCompilationUnit(CompilationUnitTree current, Parser parser)
		{
			return current.getPackageName().toString();
		}
	}

	/**
	 * Returns the binary name of the class visited.
	 *
	 * @author Gili Tzabari
	 */
	private static class GetTopClassName extends TreeScanner<String, Parser>
	{
		private String classPrefix;

		@Override
		public String visitCompilationUnit(CompilationUnitTree current, Parser parser)
		{
			if (current.getPackageName() == null)
				this.classPrefix = "";
			else
				this.classPrefix = current.getPackageName().toString() + ".";
			return super.visitCompilationUnit(current, parser);
		}

		@Override
		public String visitClass(ClassTree current, Parser parser)
		{
			return classPrefix + current.getSimpleName().toString();
		}
	}

	/**
	 * Returns the binary names of all classes that are visited.
	 *
	 * @author Gili Tzabari
	 */
	private static class GetClassNames extends SimpleTreeVisitor<Set<Identifier>, Parser>
	{
		private final String classPrefix;

		public GetClassNames(String classPrefix)
		{
			this.classPrefix = classPrefix;
		}

		@Override
		public Set<Identifier> visitCompilationUnit(CompilationUnitTree current, Parser parser)
		{
			String packageName;
			if (current.getPackageName() == null)
				packageName = "";
			else
				packageName = current.getPackageName().toString() + ".";
			Set<Identifier> result = Sets.newHashSet();
			for (Tree type: current.getTypeDecls())
			{
				for (Identifier identifier: type.accept(this, parser))
					result.add(identifier.setName(packageName + identifier.getName()));
			}
			return result;
		}

		@Override
		public Set<Identifier> visitClass(ClassTree current, Parser parser)
		{
			Set<Identifier> result = Sets.newHashSet();

			String nestedClassPrefix;
			if (current.getSimpleName().toString().isEmpty())
				nestedClassPrefix = "";
			else
			{
				Identifier className = new Identifier(classPrefix + current.getSimpleName().toString(),
					current, parser);
				nestedClassPrefix = className.getName() + "$";
			}
			for (Tree member: current.getMembers())
			{
				if (member instanceof ClassTree)
				{
					GetClassNames visitor = new GetClassNames(nestedClassPrefix);
					result.addAll(member.accept(visitor, parser));
				}
			}
			return result;
		}
	}

	/**
	 * The Parser context.
	 *
	 * @author Gili Tzabari
	 */
	private static class Parser
	{
		public final CompilationUnitTree file;
		public final SourcePositions sourcePositions;

		/**
		 * Creates a new Position instance.
		 *
		 * @param file the file being parsed
		 * @param compiler the compilation task
		 */
		public Parser(CompilationUnitTree file, CompilationTask compiler)
		{
			this.file = file;
			this.sourcePositions = Trees.instance(compiler).getSourcePositions();
		}
	}

	/**
	 * An identifier.
	 *
	 * @author Gili Tzabari
	 */
	private static class Identifier
	{
		private final String name;
		private final Tree currentTree;
		private final Parser parser;
		private final CompilationUnitTree file;
		private final SourcePositions sourcePositions;
		private final LineMap lineMap;

		/**
		 * Creates a new Identifier.
		 *
		 * @param name the identifier name
		 * @param currentTree the tree being parsed
		 * @param parser the Parser parser
		 */
		public Identifier(String name, Tree currentTree, Parser parser)
		{
			this.name = name;

			assert (!name.startsWith("name=")): "Identifier.toString() was passed in as name: " + name;
			this.currentTree = currentTree;
			this.parser = parser;
			this.file = parser.file;
			this.sourcePositions = parser.sourcePositions;
			this.lineMap = parser.file.getLineMap();
		}

		/**
		 * Sets the identifier name.
		 *
		 * @param name the new name
		 * @return the new Identifier
		 */
		public Identifier setName(String name)
		{
			if (this.name.equals(name))
				return this;
			return new Identifier(name, currentTree, parser);
		}

		/**
		 * Returns the file being parsed.
		 *
		 * @return the file being parsed
		 */
		public CompilationUnitTree getFile()
		{
			return file;
		}

		/**
		 * Returns the identifier name.
		 *
		 * @return the identifier name
		 */
		public String getName()
		{
			return name;
		}

		/**
		 * Returns the horizontal position of the beginning of a tree.
		 *
		 * @param tree the tree to look up
		 * @return the horizontal position of the beginning of the tree
		 */
		public long getX()
		{
			return lineMap.getColumnNumber(sourcePositions.getStartPosition(file, currentTree));
		}

		/**
		 * Returns the vertical position of the beginning of a tree.
		 *
		 * @param tree the tree to look up
		 * @return the vertical position of the beginning of a tree.
		 */
		public long getY()
		{
			return lineMap.getLineNumber(sourcePositions.getStartPosition(file, currentTree));
		}

		@Override
		public boolean equals(Object o)
		{
			if (!(o instanceof Identifier))
				return false;
			final Identifier other = (Identifier) o;
			return name.equals(other.name);
		}

		@Override
		public int hashCode()
		{
			int hash = 5;
			hash = 23 * hash + (this.name != null ? this.name.hashCode() : 0);
			return hash;
		}

		@Override
		public String toString()
		{
			return getClass().getName() + "[name=" + name + ", file="
						 + getFile().getSourceFile().getName() + ", x=" + getX() + ", y=" + getY() + "]";
		}
	}

	/**
	 * Returns the Java package associated with a source file.
	 *
	 * @param sourceFile the Java source file
	 * @return the package
	 * @throws FileNotFoundException if sourceFile is missing
	 * @throws IOException if an I/O error occurs while parsing the source code
	 */
	public static String getPackage(File sourceFile) throws FileNotFoundException, IOException
	{
		return parse(sourceFile, new GetPackage());
	}

	/**
	 * Returns the top-level class defined in a source-code file.
	 *
	 * @param sourceFile the Java source file
	 * @return the class binary name
	 * @throws FileNotFoundException if sourceFile is missing
	 * @throws IOException if an I/O error occurs while parsing the source code
	 */
	public static String getTopClass(File sourceFile) throws FileNotFoundException, IOException
	{
		return parse(sourceFile, new GetTopClassName());
	}

	/**
	 * Returns the classes defined in a source-code file.
	 *
	 * @param sourceFile the Java source file
	 * @return a set of binary class names
	 * @throws FileNotFoundException if sourceFile is missing
	 * @throws IOException if an I/O error occurs while parsing the source code
	 */
	public static Set<String> getClasses(File sourceFile) throws FileNotFoundException,
																															 IOException
	{
		Set<String> result = Sets.newHashSet();
		for (Identifier identifier: parse(sourceFile, new GetClassNames("")))
			result.add(identifier.getName());
		return result;
	}

	/**
	 * Returns the class files that would be generated if a source-code file were to be compiled.
	 *
	 * @param sourceFile the source code file
	 * @param defaultPackage the directory of the default package
	 * @return the paths of the class files
	 * @throws FileNotFoundException if sourceFile does not exist
	 * @throws IOException if an I/O error occurs while parsing the source file
	 */
	public static Set<File> getClassFiles(File sourceFile, File defaultPackage) throws
		FileNotFoundException, IOException
	{
		Set<File> result = Sets.newHashSet();
		for (String className: getClasses(sourceFile))
			result.add(new File(defaultPackage, className + ".class"));
		return result;
	}

	/**
	 * Returns the class file that would be generated if a source-code file were to be compiled.
	 *
	 * @param className the class binary name
	 * @param defaultPackage the directory of the default package
	 * @return the path of the class file
	 */
	public static File getClassFile(String className, File defaultPackage)
	{
		return new File(defaultPackage, className + ".class");
	}

	public static void main(String[] args) throws IOException
	{
		URLClassLoader loader = new URLClassLoader(new URL[]
			{
				new File("C:/Program Files/Java/jdk1.6.0_17/lib/tools.jar").toURI().toURL(),
				new File("C:/Users/Gili/Documents/buildinjava/trunk/netbeans/build/classes").toURI().toURL()
			});
		for (String dep: JavaSource.getDependencies(
			new File("C:/Users/Gili/Documents/buildinjava/trunk/source/buildinjava/java/OuterClass.java"),
			ImmutableList.of(new File("C:/Users/Gili/Documents/buildinjava/trunk/source")),
			loader))
		{
			System.out.println("dependency: " + dep);
		}
	}
}
